﻿using CloudStorageLight.Core;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;

namespace CloudStorageLight.Core
{
    /// <summary>
    /// 複合型Blobコンテナ
    /// </summary>
    public class CompositeBlobContainer : AzureBlobContainer
    {
        //メタデータを保持するコンテナ
        private CloudBlobContainer metaContainer;

        private string metaContainerName;
        /// <summary>
        /// 子コンテナアダプタリスト
        /// </summary>
        protected List<BlobAdapterBase> ContainerList { get; set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="containerName"></param>
        /// <param name="connectionString"></param>
        /// <param name="list"></param>
        public CompositeBlobContainer(string containerName, string connectionString, IEnumerable<BlobAdapterBase> list)
            : base(containerName, connectionString)
        {
            if (connectionString != null)
            {
                metaContainerName = containerName + "-meta";
                metaContainer = BlobClient.GetContainerReference(metaContainerName);
                metaContainer.CreateIfNotExists();
            }

            ContainerList = list.Select(x=> (BlobAdapterBase)(new RetryProxy(x).GetTransparentProxy())).ToList();
            ContainerList.Add(new AzureBlobAdapter("/", containerName, connectionString, SystemSettings.Instance.BlobEncryptKey, null));
            foreach (var item in ContainerList)
            {
                item.Parent = this;
            }
            this.BackupContainerName = metaContainerName;
            this.BackupContainer = metaContainer;
        }

        /// <summary>
        /// 指定したパスを管理しているコンテナを返す
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        private BlobAdapterBase ResolveAdapter(string path)
        {
            foreach (var item in ContainerList)
            {
                if (path.StartsWith(item.RootPath)) return item;
            }
            return ContainerList.Last();
        }

        /// <summary>
        /// 指定したパス以下のコンテナの一覧を返す
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        private IEnumerable<BlobAdapterBase> ResolveAdapters(string path)
        {
            foreach (var item in ContainerList)
            {
                if (item.RootPath.StartsWith(path) || path.StartsWith(item.RootPath)) yield return item;
            }
        }

        /// <summary>
        /// コンテナで利用している容量サイズを返す
        /// </summary>
        /// <returns></returns>
        public override async Task<long> GetContainerSizeAsync()
        {
            var size = await base.GetContainerSizeAsync();
            var metaCont = BlobClient.GetContainerReference(containerName + "-meta");
            if (await metaCont.ExistsAsync()) size += await GetContainerSizeAsync(containerName + "-meta");
            return size;
        }


        /// <summary>
        /// 指定したファイルが存在するか
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public override async Task<bool> ExistsAsync(string path)
        {
            return await ResolveAdapter(path).ExistsAsync(path);
        }


        /// <summary>
        /// Blobのコピー
        /// </summary>
        /// <param name="targetPath"></param>
        /// <param name="fromUri"></param>
        /// <returns></returns>
        public override async Task CopyAsync(BlobFile srcFile, string targetPath)
        {
            var adp =ResolveAdapter(targetPath) ;
            if (false && adp == ResolveAdapter(srcFile.Path)) //履歴付きのコピーでデータがおかしくなる件の一時対応
            {
                await adp.CopyAsync(targetPath, srcFile);
            }
            else
            {
                var tempFile = Path.GetTempFileName();
                try
                {
                    await srcFile.DownloadAsync(tempFile);
                    var targetDire = BlobItem.GetDirectoryPath(targetPath);
                    if (! await adp.ExistsAsync(targetDire))
                    {
                        await adp.CreateDirectoryAsync(new BlobDirectory(this, targetDire));
                    }
                    await adp.UploadAsync(new BlobFile(this, targetPath), tempFile);
                }
                finally
                {
                    if (File.Exists(tempFile)) File.Delete(tempFile);
                }
            }    
        }

        public override async Task CopyWithSnapshotAsync(BlobFile src, BlobFile target)
        {
            var srcBlob = GetCloudBlob(src.RelativePath);
            if (srcBlob.Exists())
            {
                await base.CopyWithSnapshotAsync(src, target);
            }

        }
        /// <summary>
        /// 削除
        /// </summary>
        /// <returns></returns>
        public override async Task<bool> DeleteAsync(BlobItem file)
        {
            var cont = ResolveAdapter(file.Path);
            if (cont.RootPath == file.Path) throw new BlobException(BlobExceptionCode.NonDeletableFolder);
            var result = await cont.DeleteAsync(file);
            return result;
        }

        /// <summary>
        /// 削除
        /// </summary>
        /// <returns></returns>
        public override async Task<bool> RecoverAsync(BlobItem file)
        {
            var cont = ResolveAdapter(file.Path);
            if (cont.RootPath == file.Path) throw new BlobException(BlobExceptionCode.NonDeletableFolder);

            var tempFile = Path.GetTempFileName();
            try
            {
                IEnumerable<IListBlobItem> listOfBlobs = BlobClient.ListBlobs(BackupContainerName + file.Path, true, BlobListingDetails.Snapshots);
                var blob = listOfBlobs.OfType<CloudBlockBlob>().Where(x => x.Name == file.RelativePath && x.SnapshotTime.HasValue).OrderByDescending(x => x.SnapshotTime.Value).FirstOrDefault();
                if (blob == null) return false;
                if (IsEncrypted)
                {
                    var blobRequestOptions = new BlobRequestOptions() { EncryptionPolicy = encryptionPolicy };
                    await blob.DownloadToFileAsync(tempFile, FileMode.Create, null, blobRequestOptions, null);
                }
                else
                {
                    await blob.DownloadToFileAsync(tempFile, FileMode.Create);
                }
                await cont.UploadAsync(file, tempFile);
            }
            finally
            {
                if (File.Exists(tempFile)) File.Delete(tempFile);
            }
            return true;
        }

        /// <summary>
        /// 物理削除
        /// </summary>
        /// <param name="file"></param>
        /// <returns></returns>
        public override async Task DeleteBlobAsync(BlobItem file)
        {

            IEnumerable<IListBlobItem> listOfBlobs = BlobClient.ListBlobs(metaContainerName + file.Path, true);
            foreach (IListBlobItem blobItem in listOfBlobs)
            {
                CloudBlockBlob theBlob = blobItem as CloudBlockBlob;
                if (theBlob != null && theBlob.Name == file.RelativePath)
                {
                    await theBlob.DeleteIfExistsAsync(DeleteSnapshotsOption.IncludeSnapshots, null, null, null);
                }
            }
        }


        /// <summary>
        /// ディレクトリののファイル一覧を取得する
        /// </summary>
        /// <param name="id"></param>
        /// <param name="maxSize"></param>
        /// <returns></returns>
        public override async Task<List<BlobItem>> GetFiles(string id, int maxSize, bool flat = false, bool deleted = false)
        {
            var metaList = await base.ListBlobs(this.metaContainer, this.metaContainerName, id, maxSize, flat, true);
            if (deleted) metaList = metaList.Where(x => x.ArchiveLastModified.HasValue).ToList();

            var children = new List<BlobItem>();
            IEnumerable<BlobAdapterBase> childList;

            if (flat)
            {
                childList = ResolveAdapters(id);
            }
            else
            {
                childList = new BlobAdapterBase[] { ResolveAdapter(id) };
            }

            foreach (var cnt in childList)
            {
                var path = cnt.RootPath.StartsWith(id) ? cnt.RootPath : id;
                var files = await cnt.GetFilesAsync(path, maxSize, flat);
                if (!deleted)
                {
                    foreach (var item in files)
                    {
                        if (!children.Any(x => x.Path == item.Path))
                        {
                            var target = metaList.FirstOrDefault(x => x.Path == item.Path);
                            if (target != null)
                            {
                                foreach (var kv in target.Metadata)
                                {
                                    cnt.MergeMetaValue(item.Metadata, kv.Key, kv.Value);
                                }

                                if (item.Metadata.ContainsKey(keyLastWriteTime)) item.LastWriteTime = ConvertTimezone(DateTimeOffset.ParseExact(item.Metadata[keyLastWriteTime], "O", null));
                                if (item.Metadata.ContainsKey(keyCreationTime)) item.CreationTime = ConvertTimezone(DateTimeOffset.ParseExact(item.Metadata[keyCreationTime], "O", null));

                            }
                            children.Add(item);
                        }
                    }
                }
                else
                {
                    foreach (var item in metaList)
                    {
                        if (!children.Any(x => x.Path == item.Path))
                        {
                            var target = files.FirstOrDefault(x => x.Path == item.Path);
                            if (target == null) children.Add(item);
                        }
                    }

                }
            }

            if (!deleted)
            {
                //サブコンテナのリストアップ
                foreach (var cont in ResolveAdapters(id))
                {
                    var p = cont.RootPath;
                    while (p.Length > id.Length)
                    {
                        var p2 = BlobItem.GetDirectoryPath(p);
                        if (id == p2)
                        {
                            if (!children.Any(x => x.Path == p))
                            {
                                var target = metaList.FirstOrDefault(x => x.Path == p);
                                if (target != null)
                                {
                                    children.Add(target);
                                }
                                else
                                {
                                    children.Add(new BlobDirectory(this, p));
                                }
                            }
                            break;
                        }
                        p = p2;
                    }
                }
            }
            return children;
        }

        /// <summary>
        /// メタ情報の設定
        /// </summary>
        /// <returns></returns>
        public override async Task SetMetadataAsync(BlobItem item)
        {
            await ResolveAdapter(item.Path).SetMetadataAsync(item);
            await base.SetMetadataAsync(item);
        }

        //protected override bool IsNotPersistMetadataKey(string key)
        //{
        //    return false;
        //}

        /// <summary>
        /// メタ情報の取得
        /// </summary>
        /// <param name="fetch"></param>
        /// <returns></returns>
        public override async Task GetMetadataAsync(BlobItem item, bool fetch = true)
        {
            await base.GetMetadataAsync(item, fetch);
            item.LastModified = null;
            item.Length = 0;
            await ResolveAdapter(item.Path).GetMetadataAsync(item);
        }

        //protected override DateTimeOffset? GetOptionalArchiveLastModified(CloudBlockBlob cloudBlob)
        //{
        //    //特別なことをしない
        //    return null;
        //}

        protected internal override CloudBlockBlob GetCloudBlob(string path, DateTimeOffset? snapshot=null)
        {
            if (string.IsNullOrEmpty(path)) return null;
            var id = path + (snapshot.HasValue ? "|" + snapshot.Value.ToString("R") : "");
            if (!CloudBlobCache.ContainsKey(id))
            {
                if (snapshot.HasValue)
                {
                    CloudBlobCache[id] = this.metaContainer.GetBlockBlobReference(path,snapshot);
                }
                else
                {
                    CloudBlobCache[id] = this.metaContainer.GetBlockBlobReference(path);
                }
            }
            return CloudBlobCache[id];
        }

        /// <summary>
        /// アップロード
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        public override async Task UploadAsync(BlobItem item, Stream stream)
        {
            await ResolveAdapter(item.Path).UploadAsync(item, stream);
        }

        /// <summary>
        /// アップロード
        /// </summary>
        /// <param name="localpath"></param>
        /// <returns></returns>
        public override async Task UploadAsync(BlobItem item, string localpath)
        {
            await ResolveAdapter(item.Path).UploadAsync(item, localpath);
        }

        public override async Task<CloudBlockBlob> CreateArchiveAsync(BlobItem item)
        {
            var cont = ResolveAdapter(item.Path);

            CloudBlockBlob lastSnapshot;
            var blob = GetCloudBlob(item.RelativePath);
            if (item.IsArchibveDirty)
            {
                var tempFile = Path.GetTempFileName();
                try
                {
                    await cont.DownloadAsync(item, tempFile);
                    if (IsEncrypted)
                    {
                        var blobRequestOptions = new BlobRequestOptions() { EncryptionPolicy = encryptionPolicy };
                        await blob.UploadFromFileAsync(tempFile, null, blobRequestOptions, null);
                    }
                    else
                    {
                        await blob.UploadFromFileAsync(tempFile);
                    }
                }
                finally
                {
                    if (File.Exists(tempFile)) File.Delete(tempFile);
                }
                lastSnapshot = await blob.CreateSnapshotAsync();
                item.LatestSnapshot = lastSnapshot.SnapshotTime;
                item.ArchiveLastModified = item.LastModified;
                await item.SetMetadataAsync(); 
            }
            else
            {
                IEnumerable<IListBlobItem> listOfBlobs = BlobClient.ListBlobs(BackupContainerName + item.Path, true, BlobListingDetails.Snapshots);
                lastSnapshot = listOfBlobs.OfType<CloudBlockBlob>().Where(x => x.Name ==  item.RelativePath &&  x.SnapshotTime.HasValue).OrderByDescending(x => x.SnapshotTime.Value).FirstOrDefault();
            }
            return lastSnapshot;

        }

        /// <summary>
        /// ダウンロード
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        public override async Task DownloadAsync(BlobItem item, Stream stream)
        {
            await ResolveAdapter(item.Path).DownloadAsync(item, stream);
        }

        /// <summary>
        /// ダウンロード
        /// </summary>
        /// <param name="localpath"></param>
        /// <returns></returns>
        public override async Task DownloadAsync(BlobItem item, string localpath)
        {
            await ResolveAdapter(item.Path).DownloadAsync(item, localpath);
        }

        /// <summary>
        /// アーカイブのダウンロード
        /// </summary>
        /// <param name="item"></param>
        /// <param name="stream"></param>
        /// <returns></returns>
        public override async Task DownloadArchiveAsync(BlobItem item, Stream stream)
        {
            var blob = this.metaContainer.GetBlockBlobReference(item.RelativePath, item.Snapshot ?? item.LatestSnapshot);

            if (IsEncrypted)
            {
                var blobRequestOptions = new BlobRequestOptions() { EncryptionPolicy = encryptionPolicy };
                await blob.DownloadToStreamAsync(stream, null, blobRequestOptions, null);
            }
            else
            {
                await blob.DownloadToStreamAsync(stream);
            }
        }

        /// <summary>
        /// フォルダ作成
        /// </summary>
        /// <param name="dire"></param>
        /// <returns></returns>
        public override async Task CreateDirectoryAsync(BlobDirectory dire)
        {
            await ResolveAdapter(dire.Path).CreateDirectoryAsync(dire);
        }

        /// <summary>
        /// コンテナの削除
        /// </summary>
        /// <returns></returns>
        public override bool Destory()
        {
            var tempCont = BlobClient.GetContainerReference(containerName + "-temp");
            tempCont.DeleteIfExists();
            var trashCont = BlobClient.GetContainerReference(containerName + "-trash");
            trashCont.DeleteIfExists();
            var catalogCont = BlobClient.GetContainerReference(containerName + "-catalog");
            catalogCont.DeleteIfExists();
            var metaCont = BlobClient.GetContainerReference(containerName + "-meta");
            metaCont.DeleteIfExists();
            var fileCont = BlobClient.GetContainerReference(containerName);
            fileCont.DeleteIfExists();

            var queueClient = StorageAccount.CreateCloudQueueClient();
            var fullqueue = queueClient.GetQueueReference(containerName + "-fullqueue");
            fullqueue.DeleteIfExists();
            var crawlqueue = queueClient.GetQueueReference(containerName + "-crawlqueue");
            crawlqueue.DeleteIfExists();

            return true;
        }


        public override async Task<bool> CheckInActionAsync(BlobUser user, BlobFile file)
        {
            return await ResolveAdapter(file.Path).CheckInActionAsync(user, file);
        }

        public override async Task<bool> CheckOutActionAsync(BlobUser user, BlobFile file)
        {
            return await ResolveAdapter(file.Path).CheckOutActionAsync(user, file);
        }

        public override Task<List<BlobItem>> ListTempFile(string id)
        {
            var children = new List<BlobItem>();
            var file = new BlobItem(null, id);
            var list = metaContainer.ListBlobs(file.RelativePath, false, BlobListingDetails.Metadata);
            if (list != null)
            {
                foreach (var item in list)
                {
                    BlobItem newItem = null;
                    var path = BlobUtil.UnescapeDataString(item.Uri.PathAndQuery).Substring(metaContainer.Uri.PathAndQuery.Length);
                    newItem = new BlobFile(this, path);
                    var blockBlob = (CloudBlockBlob)item;

                    foreach (var prop in blockBlob.Metadata)
                    {
                        newItem.Metadata[prop.Key] = blockBlob.Metadata[prop.Key];
                    }
                    newItem.Length = blockBlob.Properties.Length;

                    children.Add(newItem);
                }
            }
            return Task.FromResult(children);
        }

        public override async Task LoadTempFile(BlobItem item, Stream stream)
        {
            var blob = metaContainer.GetBlockBlobReference(item.RelativePath);
            if (IsEncrypted)
            {
                var blobRequestOptions = new BlobRequestOptions() { EncryptionPolicy = encryptionPolicy };
                await blob.DownloadToStreamAsync(stream, null, blobRequestOptions, null);
            }
            else
            {
                await blob.DownloadToStreamAsync(stream);
            }
        }

        public override async Task SaveTempFile(BlobItem item, Stream stream)
        {
            var blob = metaContainer.GetBlockBlobReference(item.RelativePath);
            if (IsEncrypted)
            {
                var blobRequestOptions = new BlobRequestOptions() { EncryptionPolicy = encryptionPolicy };
                await blob.UploadFromStreamAsync(stream, null, blobRequestOptions, null);
            }
            else
            {
                await blob.UploadFromStreamAsync(stream);
            }
        }

        public override async Task DeleteTempFile(BlobItem item)
        {
            var blob = metaContainer.GetBlockBlobReference(item.RelativePath);
            await blob.DeleteIfExistsAsync(DeleteSnapshotsOption.IncludeSnapshots, null, null, null);

        }

        public override string GetOriginalUrl(string path)
        {
            return ResolveAdapter(path).GetOriginalUrl(path);
        }

        public override CustomAuthInfo GetCustomAuth(string path)
        {
            return ResolveAdapter(path).GetCustomAuth(path);
        }

        public override void SetCustomAuth(CustomAuthInfo authInfo)
        {
            ResolveAdapter(authInfo.Path).SetCustomAuth(authInfo);
        }

        private bool disposed =false;
        protected override void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (ContainerList != null)
                {
                    foreach(var adp in ContainerList)
                    {
                        adp.Dispose();
                    }
                }
            }
            disposed = true;
            base.Dispose(disposing);            
        }
    }

}
